﻿using System.Globalization;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;

namespace AuthClientApi.OAuth.WeChat
{
    public class WeChatHandler : OAuthHandler<WeChatOptions>
    {
        public WeChatHandler(IOptionsMonitor<WeChatOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
        {

        }
        /// <summary>
        /// OAuth每次请求的Handle
        /// </summary>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public override async Task<bool> HandleRequestAsync()
        {
            /* ShouldHandleRequestAsync()  源码就是比较回调地址和请求地址是否一致   
            ShouldHandleRequestAsync()  => Task.FromResult(Options.CallbackPath == Request.Path);
            */
            if (!await ShouldHandleRequestAsync())
            {
                return false;
            }
            //拿到code回调之后会进行后续操作
            AuthenticationTicket? ticket = null;
            Exception? exception = null;
            AuthenticationProperties? properties = null;
            try
            {
                var authResult = await HandleRemoteAuthenticateAsync();
                if (authResult == null)
                {
                    exception = new InvalidOperationException("Invalid return state, unable to redirect.");
                }
                else if (authResult.Handled)
                {
                    return true;
                }
                else if (authResult.Skipped || authResult.None)
                {
                    return false;
                }
                else if (!authResult.Succeeded)
                {
                    exception = authResult.Failure ?? new InvalidOperationException("Invalid return state, unable to redirect.");
                    properties = authResult.Properties;
                }

                ticket = authResult?.Ticket;
            }
            catch (Exception ex)
            {
                exception = ex;
            }

            if (exception != null)
            {

                var errorContext = new RemoteFailureContext(Context, Scheme, Options, exception)
                {
                    Properties = properties
                };
                await Events.RemoteFailure(errorContext);

                if (errorContext.Result != null)
                {
                    if (errorContext.Result.Handled)
                    {
                        return true;
                    }
                    else if (errorContext.Result.Skipped)
                    {
                        return false;
                    }
                    else if (errorContext.Result.Failure != null)
                    {
                        throw new Exception("An error was returned from the RemoteFailure event.", errorContext.Result.Failure);
                    }
                }

                if (errorContext.Failure != null)
                {
                    throw new Exception("An error was encountered while handling the remote login.", errorContext.Failure);
                }
            }

            // We have a ticket if we get here
            var ticketContext = new TicketReceivedContext(Context, Scheme, Options, ticket)
            {
                ReturnUri = ticket.Properties.RedirectUri
            };

            ticket.Properties.RedirectUri = null;

            // Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync
            ticketContext.Properties!.Items[".AuthScheme"] = Scheme.Name;

            await Events.TicketReceived(ticketContext);

            if (ticketContext.Result != null)
            {
                if (ticketContext.Result.Handled)
                {
                    return true;
                }
                else if (ticketContext.Result.Skipped)
                {
                    return false;
                }
            }
            //这里的scheme一定要和注入服务的scheme一样
            var identity = new ClaimsIdentity(new ClaimsIdentity("Wechat"));
            //自定义的claim信息
            identity.AddClaim(new Claim("abc", "123"));
            AuthenticationProperties properties1 = new AuthenticationProperties()
            {
                //设置cookie票证的过期时间
                ExpiresUtc = DateTime.Now.AddDays(1),
                RedirectUri = "/Home/Index"
            };
            try
            {
                await Context.SignInAsync(SignInScheme, ticketContext.Principal!, ticketContext.Properties);
            }
            catch (Exception ex)
            {
                var aa = ex.Message;
            }
            //await Context.SignInAsync("Wechat", new ClaimsPrincipal(identity), properties);
            // await Context.SignInAsync(SignInScheme, ticketContext.Principal!, ticketContext.Properties);

            // Default redirect path is the base path
            if (string.IsNullOrEmpty(ticketContext.ReturnUri))
            {
                ticketContext.ReturnUri = "/";
            }

            Response.Redirect(ticketContext.ReturnUri);
            return true;
        }
        /// <summary>
        /// 第一步 获取Code之前,需要重定向的地址，可以再这里重写跳转地址的参数
        /// </summary>
        /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
        /// <param name="redirectUri">The url to redirect to once the challenge is completed.</param>
        /// <returns>The challenge url.</returns>
        protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
        {
            var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
            var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope();
            redirectUri = $"{redirectUri}?property={Options.StateDataFormat.Protect(properties)}";
            var parameters = new Dictionary<string, string>
            {
                { "appid", Options.ClientId },
                { "scope", Options.WeChatScope },
                { "response_type", "code" },
                { "redirect_uri", redirectUri },
                { "state", Options.StateAddition }
            };
            //properties.RedirectUri = redirectUri;
            if (Options.UsePkce)
            {
                var bytes = new byte[32];
                RandomNumberGenerator.Fill(bytes);
                var codeVerifier = Microsoft.AspNetCore.Authentication.Base64UrlTextEncoder.Encode(bytes);

                // Store this for use during the code redemption.
                properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);

                var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
                var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);

                parameters[OAuthConstants.CodeChallengeKey] = codeChallenge;
                parameters[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256;
            }
            //parameters["state"] = Options.StateDataFormat.Protect(properties);
            return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters!);
        }
        /// <summary>
        /// 第二步 接收到code后，获取token，再创建票据   这里可以重写需要的claims信息等等
        /// </summary>
        /// <returns></returns>
        protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            var query = Request.Query;

            var property = query["property"];
            var properties = Options.StateDataFormat.Unprotect(property);

            if (properties == null)
            {
                return HandleRequestResult.Fail("The oauth state was missing or invalid.");
            }

            // OAuth2 10.12 CSRF
            if (!ValidateCorrelationId(properties))
            {
                return HandleRequestResult.Fail("Correlation failed.", properties);
            }

            var error = query["error"];
            if (!StringValues.IsNullOrEmpty(error))
            {
                // Note: access_denied errors are special protocol errors indicating the user didn't
                // approve the authorization demand requested by the remote authorization server.
                // Since it's a frequent scenario (that is not caused by incorrect configuration),
                // denied errors are handled differently using HandleAccessDeniedErrorAsync().
                // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
                var errorDescription = query["error_description"];
                var errorUri = query["error_uri"];
                if (StringValues.Equals(error, "access_denied"))
                {
                    var result = await HandleAccessDeniedErrorAsync(properties);
                    if (!result.None)
                    {
                        return result;
                    }
                    var deniedEx = new Exception("Access was denied by the resource owner or by the remote server.");
                    deniedEx.Data["error"] = error.ToString();
                    deniedEx.Data["error_description"] = errorDescription.ToString();
                    deniedEx.Data["error_uri"] = errorUri.ToString();

                    return HandleRequestResult.Fail(deniedEx, properties);
                }

                var failureMessage = new StringBuilder();
                failureMessage.Append(error);
                if (!StringValues.IsNullOrEmpty(errorDescription))
                {
                    failureMessage.Append(";Description=").Append(errorDescription);
                }
                if (!StringValues.IsNullOrEmpty(errorUri))
                {
                    failureMessage.Append(";Uri=").Append(errorUri);
                }

                var ex = new Exception(failureMessage.ToString());
                ex.Data["error"] = error.ToString();
                ex.Data["error_description"] = errorDescription.ToString();
                ex.Data["error_uri"] = errorUri.ToString();

                return HandleRequestResult.Fail(ex, properties);
            }

            var code = query["code"];

            if (StringValues.IsNullOrEmpty(code))
            {
                return HandleRequestResult.Fail("Code was not found.", properties);
            }

            var codeExchangeContext = new OAuthCodeExchangeContext(properties, code.ToString(), BuildRedirectUri(Options.CallbackPath));
            using var tokens = await ExchangeCodeAsync(codeExchangeContext);

            if (tokens.Error != null)
            {
                return HandleRequestResult.Fail(tokens.Error, properties);
            }

            if (string.IsNullOrEmpty(tokens.AccessToken))
            {
                return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
            }
            #region 这里是写入claims
            ClaimsIdentity identity = new ClaimsIdentity(ClaimsIssuer);
            //写入用户信息
            if (Options.IsUserInfoClaim)
                identity = await ClaimsIdentity(identity, tokens);
            #endregion
            if (Options.SaveTokens)
            {
                var authTokens = new List<AuthenticationToken>();

                authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
                if (!string.IsNullOrEmpty(tokens.RefreshToken))
                {
                    authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
                }

                if (!string.IsNullOrEmpty(tokens.TokenType))
                {
                    authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
                }

                if (!string.IsNullOrEmpty(tokens.ExpiresIn))
                {
                    int value;
                    if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
                    {
                        // https://www.w3.org/TR/xmlschema-2/#dateTime
                        // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
                        var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
                        authTokens.Add(new AuthenticationToken
                        {
                            Name = "expires_at",
                            Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                        });
                    }
                }

                properties.StoreTokens(authTokens);
            }

            var ticket = await CreateTicketAsync(identity, properties, tokens);
            if (ticket != null)
            {
                return HandleRequestResult.Success(ticket);
            }
            else
            {
                return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
            }
        }
        /// <summary>
        /// 第三步 通过到code获取Token
        /// Exchanges the authorization code for a authorization token from the remote provider.
        /// </summary>
        /// <param name="context">The <see cref="OAuthCodeExchangeContext"/>.</param>
        /// <returns>The response <see cref="OAuthTokenResponse"/>.</returns>
        protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
        {
            var queryBuilder = new QueryBuilder()
            {
                { "appid", Options.ClientId },
                { "secret", Options.ClientSecret },
                { "code", context.Code },
                { "grant_type", "authorization_code" },
            };
            var url = Options.TokenEndpoint + queryBuilder.ToString();
            var request = new HttpRequestMessage(HttpMethod.Get, url);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var response = await Backchannel.SendAsync(request, Context.RequestAborted);
            response.EnsureSuccessStatusCode();
            var body = await response.Content.ReadAsStringAsync();

            if (response.IsSuccessStatusCode)
            {
                var JOBody = JObject.Parse(body);
                if (JOBody.Value<string>("errcode") == null && JOBody.Value<string>("openid") != null)
                {
                    var tokens = OAuthTokenResponse.Success(JsonDocument.Parse(body));
                    tokens.TokenType = JOBody.Value<string>("openid");
                    return tokens;
                }
                else
                {
                    return OAuthTokenResponse.Failed(new Exception("OAuth token endpoint failure"));
                }
            }
            else
                return OAuthTokenResponse.Failed(new Exception("OAuth token endpoint failure"));
        }
        protected async Task<ClaimsIdentity> ClaimsIdentity(ClaimsIdentity identity, OAuthTokenResponse tokenResponse)
        {
            var queryBuilder = new QueryBuilder()
            {
                { "access_token", tokenResponse.AccessToken },
                { "openid",  tokenResponse.TokenType },//在第二步中，openid被存入TokenType属性
                { "lang", "zh_CN" }
            };

            var infoRequest = Options.UserInformationEndpoint + queryBuilder.ToString();
            var request = new HttpRequestMessage(HttpMethod.Get, infoRequest);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var response = await Backchannel.SendAsync(request, Context.RequestAborted);
            var res = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                var JOBody = JObject.Parse(res);
                if (JOBody.Value<string>("errcode") == null && JOBody.Value<string>("openid") != null)
                {
                    var identifier = JOBody.Value<string>("openid");
                    if (!string.IsNullOrEmpty(identifier))
                    {
                        identity.AddClaim(new Claim("openid", identifier, ClaimValueTypes.String, Options.ClaimsIssuer));
                    }

                    var nickname = JOBody.Value<string>("nickname");
                    if (!string.IsNullOrEmpty(nickname))
                    {
                        identity.AddClaim(new Claim("nickname", nickname, ClaimValueTypes.String, Options.ClaimsIssuer));
                    }

                    var sex = JOBody.Value<string>("sex");
                    if (!string.IsNullOrEmpty(sex))
                    {
                        identity.AddClaim(new Claim("sex", sex, ClaimValueTypes.String, Options.ClaimsIssuer));
                    }

                    var country = JOBody.Value<string>("country");
                    if (!string.IsNullOrEmpty(country))
                    {
                        identity.AddClaim(new Claim("country", country, ClaimValueTypes.String, Options.ClaimsIssuer));
                    }

                    var province = JOBody.Value<string>("province");
                    if (!string.IsNullOrEmpty(province))
                    {
                        identity.AddClaim(new Claim("province", province, ClaimValueTypes.String, Options.ClaimsIssuer));
                    }

                    var city = JOBody.Value<string>("city");
                    if (!string.IsNullOrEmpty(city))
                    {
                        identity.AddClaim(new Claim("city", city, ClaimValueTypes.String, Options.ClaimsIssuer));
                    }

                    var headimgurl = JOBody.Value<string>("headimgurl");
                    if (!string.IsNullOrEmpty(headimgurl))
                    {
                        identity.AddClaim(new Claim("headimgurl", headimgurl, ClaimValueTypes.String, Options.ClaimsIssuer));
                    }
                }
            }
            return identity;
        }
    }
}
